MALHAWK Logo

MalHawk

Reverse Engineering
PLAT:Windows
DIFF:3.5-4.0
QUAL:4.0
2026-08-24

level 6 - 10

Uploading remaining 5 crackmes under 1

This writeup covers the remaining levels from 6 to 10. Each level continues to introduce new checks: out-of-order byte checks, a 3-branch verification loop, multi-byte algebraic constraints, and multithreading with a rolling string hash. Across all the levels, flag gen comes first, then length check and finally branching to their own individual checks.


Level 6: Out-of-order character verification

Checks a 5-letter key, but instead of checking the string sequentially, it verifies the characters out of order across the input buffer.

Level 6 character checks

Looking at the disassembly, the verification steps are:

  • Index 0 (byte ptr [rax]): checked against 0x63 ('c')
  • Index 4 (byte ptr [rax+4]): checked against 0x6B ('k')
  • Index 1 (byte ptr [rax+1]): checked against 0x72 ('r')
  • Index 3 (byte ptr [rax+3]): checked against 0x63 ('c')
  • Index 2 (byte ptr [rax+2]): checked against 0x61 ('a')

Reconstructing the indices in order (0, 1, 2, 3, 4) gives the password: crack.

Level 6 terminal success


Level 7: XOR encrypted password check

Involves a check against an XOR encrypted password.

Level 7 string references

Level 7 length check

Level 7 XOR loop

If length check matches, it runs an XOR verification loop:

  1. Loads our input character: movzx eax, byte ptr [rax+rcx]
  2. Encrypts it: xor al, 5Ah
  3. Compares the encrypted byte against the stored reference byte: cmp al, [rcx+rdx]
  4. Loops until all characters are checked

XORing the stored reference bytes with 0x5A gives the password: license.

Level 7 terminal success

The menu is just used as a manual push button to exit the program.


Level 8: 3 intervals, 3 branches (switch case)

Involves a looped verification check that branches based on a 54-byte array.

Part 2: Password Verification Loop

After flag generation, it moves on to verify our password against a 54-byte constant array loaded into R10.

This 54-byte array is structured into 18 intervals of 3 bytes each (18 * 3 = 54 bytes). The loop steps through this array one 3-byte interval at a time (add rdx, 3).

Level 8 verification flow

In every 3-byte interval, the first byte acts as an interval selector (1, 2, or 3) that dictates which branch the program takes:

  • Interval 1 (fetch_password_char_by_char): When the interval selector is 1, it takes the index byte and fetches the corresponding character from our input password one by one (movzx r9d, byte ptr [rax+r8]).
  • Interval 2 (pass_encryption): When the interval selector is 2, it takes the input password char and XORs it with a key byte from the interval (xor r9b, r8b).
  • Interval 3 (verify_pass_encryption_char_by_char): When the interval selector is 3, it compares the XOR result against the expected byte (cmp r9b, r8b). If they match, cmovz ecx, eax preserves 1 in r11b.

At the end of the loop, test r11b, r11b checks r11. If any character check failed, r11 is 0 and branches to sad_path. If all passed, r11 is 1 and it takes the happy path.


Password Recovery

To find each expected character, we reverse the XOR by taking the key byte and the expected result byte from the array. At runtime, the binary takes our input character, XORs it with the key byte, and compares the result to the expected byte:

Char Index Key Byte Expected Byte Reverse Math (Key ^ Expected) Plaintext Char Runtime Check (Char ^ Key == Expected)
0 0x12 0x7F 0x12 ^ 0x7F = 0x6D 'm' 'm' (0x6D) ^ 0x12 == 0x7F
1 0x34 0x55 0x34 ^ 0x55 = 0x61 'a' 'a' (0x61) ^ 0x34 == 0x55
2 0x56 0x22 0x56 ^ 0x22 = 0x74 't' 't' (0x74) ^ 0x56 == 0x22
3 0x78 0x0A 0x78 ^ 0x0A = 0x72 'r' 'r' (0x72) ^ 0x78 == 0x0A
4 0x90 0xF9 0x90 ^ 0xF9 = 0x69 'i' 'i' (0x69) ^ 0x90 == 0xF9
5 0xBC 0xC4 0xBC ^ 0xC4 = 0x78 'x' 'x' (0x78) ^ 0xBC == 0xC4

This gives the full 6-letter password: matrix.

Level 8 terminal success


Level 9: 32-bit algebra gymnastics

Takes an 8-letter password and divides it into two 32-bit integers, subjecting them to arithmetic and bitwise constraints.

Level 9 disassembly

The program splits the 8-byte input into two 4-byte halves:

  • mov ecx, [rax]: First 4 bytes (X)
  • mov edx, [rax+4]: Second 4 bytes (Y)

The verification sequence is:

lea  eax, [rdx+rcx]        ; eax = Y + X
xor  edx, eax              ; edx = Y ^ (Y + X)
lea  ecx, [rdx+1337h]      ; ecx = (Y ^ (Y + X)) + 0x1337
xor  ecx, eax              ; ecx = ((Y ^ (Y + X)) + 0x1337) ^ (Y + X)
cmp  ecx, 656D4278h        ; must equal 0x656D4278
jnz  sad_path
lea  eax, [rdx+rcx]        ; eax = edx + ecx
cmp  eax, 22F8D52Ch        ; must equal 0x22F8D52C
jnz  sad_path

let us work from known to unknown, all we have are two constants -> 0x656D4278 and 0x22F8D52C. We know the final correct value inside eax should be 0x22F8D52C. To form eax we add edx and ecx. We know ecx must be 0x656D4278, so:

  1. edx = 0x22F8D52C - 0x656D4278 = 0xBD8B92B4 (result of the first XOR op)
  2. (edx + 0x1337) ^ eax = 0x656D4278, which gives eax = 0xD8E6E793 (sum of the two halves)
  3. EDX = 0xBD8B92B4 ^ 0xD8E6E793 = 0x656D7527 (the last 4 bytes at rax+4)
  4. ECX = 0xD8E6E793 - 0x656D7527 = 0x7379726C (the first 4 bytes at rax)

Converting both values to little-endian byte order:

  • First 4 bytes (ECX = 0x7379726C): in little-endian bytes 6C 72 79 73, which is lrys
  • Last 4 bytes (EDX = 0x656D7527): in little-endian bytes 27 75 6D 65, which is 'ume

Placing the first 4 bytes followed by the last 4 bytes gives the full 8-letter password: lrys'ume.

Level 9 terminal success


Level 10: Multithreading and rolling string hash

In the main function, the check itself is clear: it compares a value in eax against 0x3D17141A. But where did the eax value come from? There was no calculation loop in the main function itself, yet the value being compared kept changing based on our password input.

Tracing back through main revealed a call to _Thrd_join - the main program was waiting for a background worker thread:

Level 10 Thrd_join call

To find where the hash was being written, I checked cross-references (xrefs) on the global variable dword_7FF6C38E6278 and looked for the instruction with type w (Write): to see what instruction is writing to it.

Level 10 xrefs write

The w xref pointed directly to sub_7FF6C38E1300+86, which was moving a value from ebx to the variable and i eventually tracked what was setting ebx and landed on the worker thread function containing the calculation loop:

Level 10 hash loop

Inside the worker function, the loop (hash_gen:) computes a rolling hash across each character:

movsx eax, byte ptr [rdx]  ; Load current character
inc   rdx
imul  ebx, 1Fh             ; multiply by 31 (0x1F)
add   ebx, eax             ; add next character
cmp   rdx, rcx
jnz   short hash_gen

In the forward direction, each cycle is just:

  1. a * 0x1F = c (where a is the running hash, and 0x1F is 31 in decimal)
  2. c + d = e (where d is the ASCII value of the next character, and e is the new hash)

Level 10 store hash

Once computed, the worker thread writes the final hash into dword_7FF6A9536278 and unlocks its mutex (_Mtx_unlock).

Level 10 compare hash

Back in the main thread, once _Thrd_join finishes, it reads that global variable and checks if it equals 0x3D17141A.

Once again working from known to unknown. Our final target hash is 0x3D17141A, which is 1,024,922,650 in decimal.

Reversing the cycle equation: 1,024,922,650 - d = 31 * a

Dividing 1,024,922,650 by 31: 1,024,922,650 / 31 = 33,062,020 with a remainder of 30.

For (1,024,922,650 - d) to be cleanly divisible by 31, our character d must be 30 plus multiples of 31:

  • 30 + 31 = 61 (which is '=', hex 0x3D)
  • Then a = (1,024,922,650 - 61) / 31 = 33,062,019 (0x01F884E3)

Now 33,062,019 becomes our new target hash, and we repeat this exact same step backwards until the hash reaches 0.

Level 10 solver script

Wrote a script that does the math for me, and because of the multiples of 31 at each step, the reverse script produces 243 valid strings.

Level 10 terminal success